package com.ieducc.ffmpegdemo.ffmpegutil;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.utils.DateUtils;
import org.springframework.stereotype.Service;

import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Date;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @desc ffmprg 工具类
 */
@Service
@Slf4j
public class FfmpegService {

    private static final Pattern FORMAT_PATTERN = Pattern
            .compile("^\\s*([D ])([E ])\\s+([\\w,]+)\\s+.+$");

    private static final Pattern ENCODER_DECODER_PATTERN = Pattern.compile(
            "^\\s*([D ])([E ])([AVS]).{3}\\s+(.+)$", Pattern.CASE_INSENSITIVE);

    private static final Pattern PROGRESS_INFO_PATTERN = Pattern.compile(
            "\\s*(\\w+)\\s*=\\s*(\\S+)\\s*", Pattern.CASE_INSENSITIVE);

    private static final Pattern SIZE_PATTERN = Pattern.compile(
            "(\\d+)x(\\d+)", Pattern.CASE_INSENSITIVE);

    private static final Pattern FRAME_RATE_PATTERN = Pattern.compile(
            "([\\d.]+)\\s+(?:fps|tb\\(r\\))", Pattern.CASE_INSENSITIVE);

    private static final Pattern BIT_RATE_PATTERN = Pattern.compile(
            "(\\d+)\\s+kb/s", Pattern.CASE_INSENSITIVE);

    private static final Pattern SAMPLING_RATE_PATTERN = Pattern.compile(
            "(\\d+)\\s+Hz", Pattern.CASE_INSENSITIVE);

    private static final Pattern CHANNELS_PATTERN = Pattern.compile(
            "(mono|stereo)", Pattern.CASE_INSENSITIVE);

    private static final Pattern SUCCESS_PATTERN = Pattern.compile(
            "^\\s*video\\:\\S+\\s+audio\\:\\S+\\s+global headers\\:\\S+.*$",
            Pattern.CASE_INSENSITIVE);

    private FFMPEGLocator locator;

    public FfmpegService() {
        this.locator = new DefaultFFMPEGLocator();
    }

    public FfmpegService(FFMPEGLocator locator) {
        this.locator = locator;
    }

    /**
     * @param filePath
     * @return
     * @throws InputFormatException
     * @throws EncoderException
     * @desc 获取视频信息
     */
    public MultimediaInfo getInfoByFile(String filePath) throws InputFormatException,
            EncoderException {
        File source = new File(filePath);
        FFMPEGExecutor ffmpeg = locator.createExecutor();
        ffmpeg.addArgument("-i");
        ffmpeg.addArgument(source.getAbsolutePath());
        try {
            ffmpeg.execute();
        } catch (IOException e) {
            throw new EncoderException(e);
        }
        try {
            RBufferedReader reader = null;
            reader = new RBufferedReader(new InputStreamReader(ffmpeg
                    .getErrorStream()));
            return parseMultimediaInfo(source.getAbsolutePath(), reader);
        } finally {
            ffmpeg.destroy();
        }
    }

    public static void main(String[] args) {
        String filePath = "https://test.ieducc.com/yr2awpn6xep1637543708781.mp4";

        try {
            final int bitRate = new FfmpegService().getInfoByUrl(filePath).getVideo().getBitRate();

            System.out.println("bitRate==========>>>" + bitRate);
        } catch (EncoderException e) {
            e.printStackTrace();
        }
    }

    /**
     * @param url
     * @return
     * @throws InputFormatException
     * @throws EncoderException
     * @desc 获取线上视频的基础信息
     */
    public MultimediaInfo getInfoByUrl(String url) throws InputFormatException,
            EncoderException {
        FFMPEGExecutor ffmpeg = locator.createExecutor();

        ffmpeg.addArgument("-i");
        ffmpeg.addArgument(url);
        try {
            ffmpeg.execute();
        } catch (IOException e) {
            throw new EncoderException(e);
        }
        try {
            RBufferedReader reader = null;
            reader = new RBufferedReader(new InputStreamReader(ffmpeg
                    .getErrorStream()));
            return parseMultimediaInfo(url, reader);
        } finally {
            ffmpeg.destroy();
        }
    }

    /**
     * @param input   视频文件或链接
     * @param time    截取的固定时间
     * @param imgPath 图片保存位置
     * @return
     * @throws EncoderException
     * @desc 视频截图
     */
    public boolean videoScreenshot(String input, String time, String imgPath) throws EncoderException {
        FFMPEGExecutor ffmpeg = locator.createExecutor();
        ffmpeg.addArgument("-i");
        ffmpeg.addArgument(input);
        ffmpeg.addArgument("-ss");
        ffmpeg.addArgument(time);
        ffmpeg.addArgument("-vframes");
        ffmpeg.addArgument("1");
        ffmpeg.addArgument(imgPath);
        try {
            CmdResult cmdResult = ffmpeg.execute2();
            return cmdResult.isSuccess();
        } catch (Exception e) {
            throw new EncoderException(e);
        } finally {
            ffmpeg.destroy();
        }
    }

    /**
     * @param url
     * @param output
     * @param userAgent
     * @param headers
     * @return
     * @throws InputFormatException
     * @throws EncoderException
     * @desc 合并M3U8视频
     */
    public boolean mergeM3U8Video(String url, String output, String userAgent, Map<String, String> headers) throws InputFormatException,
            EncoderException {
        FFMPEGExecutor ffmpeg = locator.createExecutor();
        if (StringUtils.isNotBlank(userAgent)) {
            ffmpeg.addArgument("-user_agent");
            ffmpeg.addArgument(userAgent);
        }
        if (null != headers && headers.size() > 0) {
            ffmpeg.addArgument("-headers");
            StringBuilder sb = new StringBuilder();
            for (Map.Entry<String, String> entry : headers.entrySet()) {
                if (StringUtils.isNotBlank(entry.getKey()) && StringUtils.isNotBlank(entry.getValue())) {
                    sb.append(entry.getKey()).append(":").append(entry.getValue()).append("$'\\r\\n'");
                }
            }
            ffmpeg.addArgument(sb.substring(0, sb.length() - 7));
        }

        ffmpeg.addArgument("-i");
        ffmpeg.addArgument(url);
        ffmpeg.addArgument("-vcodec");
        ffmpeg.addArgument("copy");
        ffmpeg.addArgument("-acodec");
        ffmpeg.addArgument("copy");
        ffmpeg.addArgument("-absf");
        ffmpeg.addArgument("aac_adtstoasc");

        ffmpeg.addArgument(output);
        try {
            CmdResult cmdResult = ffmpeg.execute2();
            return cmdResult.isSuccess();
        } catch (Exception e) {
            throw new EncoderException(e);
        } finally {
            ffmpeg.destroy();
        }
    }

    /**
     * @param output
     * @param input  合并视频
     * @return
     * @throws EncoderException
     * @desc 合并视频
     */
    public boolean mergeVideo(String output, String... input) throws EncoderException {
        FFMPEGExecutor ffmpeg = locator.createExecutor();
        for (String str : input) {
            ffmpeg.addArgument("-i");
            ffmpeg.addArgument(str);
        }
        ffmpeg.addArgument("-filter_complex");
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < input.length; i++) {
            sb.append("[" + i + ":v:0][" + i + ":a:0]");
        }
        sb.append("concat=n=" + input.length + ":v=1:a=1[outv][outa]");
        ffmpeg.addArgument(sb.toString());
        ffmpeg.addArgument("-map");
        ffmpeg.addArgument("[outv]");
        ffmpeg.addArgument("-map");
        ffmpeg.addArgument("[outa]");
        ffmpeg.addArgument(output);

        try {
            CmdResult cmdResult = ffmpeg.execute2();
            return cmdResult.isSuccess();
        } catch (Exception e) {
            throw new EncoderException(e);
        } finally {
            ffmpeg.destroy();
        }
    }

    /**
     * @param inout
     * @param output
     * @return
     * @throws EncoderException
     * @desc 视频转码，转成 h264 (High) (avc1 / 0x31637661)
     */
    public boolean formatToH264(String inout, String output) throws EncoderException {
        FFMPEGExecutor ffmpeg = locator.createExecutor();
        ffmpeg.addArgument("-i");
        ffmpeg.addArgument(inout);
        ffmpeg.addArgument("-map");
        ffmpeg.addArgument("0");
        ffmpeg.addArgument("-c:v");
        ffmpeg.addArgument("libx264");
        ffmpeg.addArgument("-crf");
        ffmpeg.addArgument("18");
        ffmpeg.addArgument("-c:a");
        ffmpeg.addArgument("copy");
        ffmpeg.addArgument(output);
        try {
            CmdResult cmdResult = ffmpeg.execute2();
            return cmdResult.isSuccess();
        } catch (Exception e) {
            throw new EncoderException(e);
        } finally {
            ffmpeg.destroy();
        }
    }

    /**
     * @param output 输出文件
     * @param input  输入文件
     * @return
     * @throws EncoderException
     * @desc 压缩视频文件
     */
    public boolean compressVideo(String input, String output) throws EncoderException {
        log.info("compressVideo方法开始执行了===========>>>" + DateUtils.formatDate(new Date(), "yyyy-MM-dd HH:mm:ss"));
        FFMPEGExecutor ffmpeg = locator.createExecutor();
        int width = 1080;// 宽度
        int height = 720;// 高度
        ffmpeg.addArgument("-i");
        ffmpeg.addArgument(input);
        ffmpeg.addArgument("-vf");
        ffmpeg.addArgument("scale=w=" + width + ":h=" + height);
        ffmpeg.addArgument("-b:v");
        ffmpeg.addArgument("-420");
        ffmpeg.addArgument("-b:a");
        ffmpeg.addArgument("-64");
        ffmpeg.addArgument(output);

        try {
            CmdResult cmdResult = ffmpeg.execute2();
            log.info("compressVideo方法执行结束了===========>>>" + DateUtils.formatDate(new Date(), "yyyy-MM-dd HH:mm:ss"));
            return cmdResult.isSuccess();
        } catch (Exception e) {
            throw new EncoderException(e);
        } finally {
            ffmpeg.destroy();
        }

    }


    private MultimediaInfo parseMultimediaInfo(String absolutePath,
                                               RBufferedReader reader) throws InputFormatException,
            EncoderException {
        Pattern p1 = Pattern.compile("^\\s*Input #0, (\\w+).+$\\s*",
                Pattern.CASE_INSENSITIVE);
        Pattern p2 = Pattern.compile(
                "^\\s*Duration: (\\d\\d):(\\d\\d):(\\d\\d)\\.(\\d).*$",
                Pattern.CASE_INSENSITIVE);
        Pattern p3 = Pattern.compile(
                "^\\s*Stream #\\S+: ((?:Audio)|(?:Video)|(?:Data)): (.*)\\s*$",
                Pattern.CASE_INSENSITIVE);
        MultimediaInfo info = null;
        try {
            int step = 0;
            String line = null;
            while ((line = reader.readLine()) != null) {

                if (step == 0) {
                    String token = absolutePath + ": ";
                    if (line.startsWith(token)) {
                        String message = line.substring(token.length());
                        throw new InputFormatException(message);
                    }
                    Matcher m = p1.matcher(line);
                    if (m.matches()) {
                        String format = m.group(0);
                        info = new MultimediaInfo();
                        info.setFormat(format);
                        step++;
                    }
                }

                if (step > 0) {
                    Matcher m1 = p2.matcher(line);
                    if (m1.matches()) {
                        long hours = Integer.parseInt(m1.group(1));
                        long minutes = Integer.parseInt(m1.group(2));
                        long seconds = Integer.parseInt(m1.group(3));
                        long dec = Integer.parseInt(m1.group(4));
                        long duration = (dec * 100L) + (seconds * 1000L)
                                + (minutes * 60L * 1000L)
                                + (hours * 60L * 60L * 1000L);
                        info.setDuration(duration);
                    }
                    Matcher m = p3.matcher(line);
                    if (m.matches()) {
                        String type = m.group(1);
                        String specs = m.group(2);
                        if ("Video".equalsIgnoreCase(type)) {
                            VideoInfo video = new VideoInfo();
                            StringTokenizer st = new StringTokenizer(specs, ",");
                            for (int i = 0; st.hasMoreTokens(); i++) {
                                String token = st.nextToken().trim();
                                if (i == 0) {
                                    video.setDecoder(token);
                                } else {
                                    boolean parsed = false;
                                    // Video size.
                                    Matcher m2 = SIZE_PATTERN.matcher(token);
                                    if (!parsed && m2.find()) {
                                        int width = Integer.parseInt(m2
                                                .group(1));
                                        int height = Integer.parseInt(m2
                                                .group(2));
                                        video.setSize(new VideoSize(width,
                                                height));
                                        parsed = true;
                                    }
                                    // Frame rate.
                                    m2 = FRAME_RATE_PATTERN.matcher(token);
                                    if (!parsed && m2.find()) {
                                        try {
                                            float frameRate = Float
                                                    .parseFloat(m2.group(1));
                                            video.setFrameRate(frameRate);
                                        } catch (NumberFormatException e) {
                                            e.printStackTrace();
                                        }
                                        parsed = true;
                                    }
                                    // Bit rate.
                                    m2 = BIT_RATE_PATTERN.matcher(token);
                                    if (!parsed && m2.find()) {
                                        int bitRate = Integer.parseInt(m2
                                                .group(1));
                                        video.setBitRate(bitRate);
                                        parsed = true;
                                    }
                                }
                            }
                            info.setVideo(video);
                        } else if ("Audio".equalsIgnoreCase(type)) {
                            AudioInfo audio = new AudioInfo();
                            StringTokenizer st = new StringTokenizer(specs, ",");
                            for (int i = 0; st.hasMoreTokens(); i++) {
                                String token = st.nextToken().trim();
                                if (i == 0) {
                                    audio.setDecoder(token);
                                } else {
                                    boolean parsed = false;
                                    // Sampling rate.
                                    Matcher m2 = SAMPLING_RATE_PATTERN
                                            .matcher(token);
                                    if (!parsed && m2.find()) {
                                        int samplingRate = Integer.parseInt(m2
                                                .group(1));
                                        audio.setSamplingRate(samplingRate);
                                        parsed = true;
                                    }
                                    // Channels.
                                    m2 = CHANNELS_PATTERN.matcher(token);
                                    if (!parsed && m2.find()) {
                                        String ms = m2.group(1);
                                        if ("mono".equalsIgnoreCase(ms)) {
                                            audio.setChannels(1);
                                        } else if ("stereo"
                                                .equalsIgnoreCase(ms)) {
                                            audio.setChannels(2);
                                        }
                                        parsed = true;
                                    }
                                    // Bit rate.
                                    m2 = BIT_RATE_PATTERN.matcher(token);
                                    if (!parsed && m2.find()) {
                                        int bitRate = Integer.parseInt(m2
                                                .group(1));
                                        audio.setBitRate(bitRate);
                                    }
                                }
                            }
                            info.setAudio(audio);
                        }
                    }
                }

            }
        } catch (IOException e) {
            throw new EncoderException(e);
        }
        if (info == null) {
            throw new InputFormatException();
        }
        return info;
    }

}
